Logging in JAVA
contents
자바에서 로그를 남기는 건 역사적인 이유로 인해 수많은 라이브러리(Log4j, Logback, SLF4J, JUL, JCL 등)가 존재하기 때문에 처음 접하면 매우 헷갈립니다.
스프링에서의 로깅을 이해하려면 먼저 "퍼사드 패턴(Facade Pattern)" 을 이해해야 합니다.
다음은 자바 로깅 아키텍처와 스프링 부트에서의 설정 방법입니다.
1. 아키텍처: 인터페이스(Interface) vs 구현체(Implementation)
자바에서는 "API"(코드에 작성하는 것) 와 "엔진"(실제로 로그를 기록하는 것) 을 분리합니다.
A. 퍼사드 (인터페이스)
여러분은 항상 퍼사드(Facade) 를 보고 코딩해야 합니다. 그래야 나중에 자바 코드를 수정하지 않고도 로깅 라이브러리(엔진)만 교체할 수 있습니다.
- SLF4J (Simple Logging Facade for Java): 현재 업계 표준입니다. 무조건 이것을 씁니다.
- JCL (Jakarta Commons Logging): 예전 표준이며 스프링 프레임워크 내부적으로 사용되지만, 보통 SLF4J로 연결(Bridge)하여 사용합니다.
B. 구현체 (엔진)
실제로 파일 쓰기(IO) 작업을 수행하는 라이브러리입니다.
- Logback: 스프링 부트의 기본 엔진입니다. 빠르고, SLF4J의 네이티브 구현체이며, Log4j 창시자가 만들었습니다.
- Log4j2: "신흥 강자"입니다. 비동기 로깅(LMAX Disruptor 기술)을 지원하여 엄청난 처리량(Throughput)을 자랑합니다.
- JUL (java.util.logging): JDK에 내장되어 있지만, 사용하기 불편해서 실무에서는 거의 쓰지 않습니다.
2. 코드에서 사용하는 법 (Lombok)
현대적인 스프링 부트 개발에서는 로거를 수동으로 생성하는 일이 드뭅니다. Lombok을 사용하세요.
수동 방식 (보일러플레이트 코드):
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PaymentService {
// 매번 이 줄을 작성해야 함
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
public void pay() {
log.info("결제 처리 중");
}
}
Lombok 방식 (권장):
import lombok.extern.slf4j.Slf4j;
@Slf4j // 자동으로 'log' 필드를 생성해 줌
@Service
public class PaymentService {
public void pay() {
log.info("결제 처리 중");
}
}
3. 로그 레벨 (계층 구조)
상용 환경 디버깅을 위해 레벨 이해는 필수입니다. 레벨을 INFO로 설정하면, 그보다 낮은 레벨의 로그는 전부 무시됩니다.
- TRACE: 극도로 상세함 (예: 루프 변수 값). "for문 인덱스 i=4".
- DEBUG: 개발자용 정보 (예: 페이로드 내용). "User 객체 데이터: {name: '철수'}".
- INFO: 정상적인 애플리케이션 흐름 (기본값). "애플리케이션 시작됨", "사용자 로그인".
- WARN: 잠재적 문제지만 앱은 계속 동작함. "디스크 공간 90% 참", "DB 연결 재시도 중".
- ERROR: 뭔가 고장 남. "NullPointerException 발생", "DB 연결 불가".
설정 (application.yml):
logging:
level:
root: INFO # 전체 기본값
com.example.myproject: DEBUG # 내 코드만 DEBUG 레벨로 자세히 보기
org.springframework.web: WARN # 스프링 내부 로그는 시끄러우니까 줄이기
4. Logback 설정하기 (logback-spring.xml)
application.yml은 간단한 설정에는 좋지만, 상용 환경에서는 로그 회전(Rolling) 전략(디스크가 꽉 차지 않게 매일 새 파일 생성)과 커스텀 패턴이 필요합니다. 이를 위해 src/main/resources/logback-spring.xml 파일을 만듭니다.
설정 핵심 개념:
- Appender: 로그를 어디에 출력할지 (콘솔, 파일, 소켓, DB 등).
- Encoder/Pattern: 로그를 어떤 모양으로 찍을지.
- Policy: 언제 새 파일을 만들지 (시간 기준, 용량 기준).
logback-spring.xml 예시:
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logs/app.log
logs/app-%d{yyyy-MM-dd}.%i.log
10MB
30
%d %-5level [%X{requestId}] %logger{35} - %msg%n
5. 심화: MDC (Mapped Diagnostic Context)
멀티 스레드 웹 서버에서 필수적인 기술입니다.
여러 사용자가 동시에 서버에 요청을 보내면, 로그 파일에 여러 사용자의 로그가 뒤섞이게 됩니다. 특정 사용자의 요청 하나만 추적하려면 어떻게 해야 할까요?
MDC는 ThreadLocal을 사용하는 맵(Map)과 같습니다. 요청이 시작될 때 "요청 ID"를 넣어두면, 해당 스레드에서 찍히는 모든 로그에 자동으로 그 ID가 포함됩니다.
인터셉터(Interceptor) 예시:
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String uuid = UUID.randomUUID().toString();
MDC.put("requestId", uuid); // ThreadLocal에 저장
return true;
}
@Override
public void afterCompletion(...) {
MDC.clear(); // 중요: 메모리 누수 방지를 위해 반드시 비워야 함!
}
}
로그 출력 결과:
2023-10-01 INFO [a1b2-c3d4] PaymentService - 처리 중...
2023-10-01 INFO [a1b2-c3d4] Database - 저장 중...
이제 grep a1b2-c3d4 명령어로 해당 사용자의 여정만 쏙 뽑아볼 수 있습니다.
6. 베스트 프랙티스
- PII(개인식별정보) 로깅 금지: 비밀번호, 카드 번호, 주민등록번호는 절대 로그에 남기면 안 됩니다.
System.out.println절대 금지: 레벨 제어도 안 되고, 타임스탬프도 없고, 성능상 스레드 락(Lock)을 유발할 수 있습니다. 무조건log.info()를 쓰세요.- Placeholders(
{}) 사용:- ❌ 나쁨:
log.debug("User " + user.getName() + " logged in");(DEBUG가 꺼져 있어도 문자열 더하기 연산이 실행됨). - ✅ 좋음:
log.debug("User {} logged in", user.getName());(DEBUG가 켜져 있을 때만 연산 수행).
- ❌ 나쁨:
- 비동기 로깅 (Async Logging): 트래픽이 매우 많은 시스템이라면, 디스크 쓰기 작업이 스레드를 멈추게(Blocking) 할 수 있습니다. Log4j2 Async Appender를 사용하여 별도의 스레드에서 로그를 쓰게 하면 사용자 응답 속도에 영향을 주지 않습니다.
references